package za.co.chaotik.tron;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import za.co.chaotik.tron.exceptions.IllegalMoveException;
import za.co.chaotik.tron.exceptions.LostException;
import za.co.chaotik.tron.exceptions.OutOfTurnException;
import za.co.chaotik.tron.exceptions.TronException;
import za.co.chaotik.tron.exceptions.UnsupportedNumberOfPlayersException;
import za.co.chaotik.tron.patterns.Subject;

import com.badlogic.gdx.graphics.Color;

public abstract class AbstractMap extends Subject {
	/** Map width in positions. */
	private int width;
	
	/** Map height in positions. */
	private int height;
	
	/** 2D array of map positions. */
	protected List<List<Position>> positions;
	
	/** List of players. */
	protected List<Player> players;
	
	/** List of dead players */
	protected List<Player> deadPlayers;

	/** List of cycles (could potentially have multiple cycles per player). */
	protected List<Cycle> cycles;
	
	protected Map<Player, Cycle> playerCycles;
	
	/** List of player moves. */
	protected Map<Player, List<Position>> moves;
	
	/** Index of the current player in the players list. */
	protected int currentPlayer = 0;
	
	public AbstractMap(int width, int height, int numPlayers) {
		this.width = width;
		this.height = height;
		
		List<String> playerNames = new ArrayList<String>();
		List<Color> colors = new ArrayList<Color>();
		colors.add(Color.RED); playerNames.add("RED");
		colors.add(Color.BLUE); playerNames.add("BLUE");
		colors.add(Color.GREEN); playerNames.add("GREEN");
		colors.add(Color.MAGENTA); playerNames.add("MAGENTA");
		
		playerCycles = new HashMap<Player, Cycle>();
		moves = new HashMap<Player, List<Position>>();
		players = new ArrayList<Player>(numPlayers);
		deadPlayers = new ArrayList<Player>(numPlayers - 1);
		cycles = new ArrayList<Cycle>(numPlayers);
		for (int i = 0; i < numPlayers; i++) {
			Player player = new Player(playerNames.get(i));
			player.setColor(colors.get(i));
			players.add(player);
			Cycle cycle = new Cycle(player);
			cycles.add(cycle);
			playerCycles.put(player, cycle);
			moves.put(player, new ArrayList<Position>());
		}
	}
		
	/**
	 * Get the map width in Positions.
	 * 
	 * @return the map width (in Positions)
	 */
	public int getWidth() {
		return width;
	}
	
	/**
	 * Get the map height in Positions.
	 * 
	 * @return the map height (in Positions)
	 */
	public int getHeight() {
		return height;
	}
	
	/**
	 * Get the position at the given map coordinate.
	 * 
	 * @param x map x coordinate (in positions)
	 * @param y map y coordinate (in positions)
	 * @return the Position at (x, y)
	 */
	public Position getPosition(int x, int y) {
		if ((y < 0) || (y >= height) || (x < 0) || (x >= width)) {
			return null;
		}

		return positions.get(y).get(x);
	}
	
	/**
	 * Move the specified cycle to the target position. Generally the rule
	 * is that this should be an empty position adjacent to the cycle - moving
	 * to an occupied position will lose the game, as will attempting to move
	 * further than 1 distance in a single move.
	 *  
	 * @param cycle
	 * @param targetPos
	 */
	public void moveCycle(Cycle cycle, Position targetPos) throws TronException {
		boolean lost = false;
		
		// Make sure players don't move out of turn
		if (!cycle.getOwner().equals(getCurrentPlayer())) {
			throw new OutOfTurnException(cycle.getOwner().getName() + " tried to move out of turn - it is actually " + getCurrentPlayer().getName() + "'s turn.");
		}
		
		// Make sure the move is legal (distance must be 1)
		List<Position> validMoves = getValidMoves(cycle);
		if (!validMoves.contains(targetPos)) {
			throw new IllegalMoveException(cycle.getOwner().getName() + 
					" tried to move from (" + cycle.getX() + ", " +
					cycle.getY() + ") to (" + targetPos.getX() + ", " + 
					targetPos.getY() + ").");
		}
		
		// Check for a losing move
		if (targetPos.getValue() != null) {
			lost = true;
		}
		
		// Put a wall in the old position
		getPosition(cycle.getX(), cycle.getY()).setValue(new Wall(cycle.getOwner()));
		
		// Put the cycle in the new position
		targetPos.setValue(cycle);
		cycle.setPosition(targetPos.getX(), targetPos.getY());
		
		// Update the move history
		moves.get(cycle.getOwner()).add(targetPos);
		
		// Next player's turn
		nextPlayer();
		
		// Update observers
		changed();
		
		if ((lost) || (getNonLosingMoves(cycle).size() == 0)) {
			deadPlayers.add(cycle.getOwner());
			players.remove(cycle.getOwner());
			
			// NOTE TESTED: In theory this will make the correct player go next in 3+ player games
			if (currentPlayer != 0) {
				currentPlayer--;
			}
			
			throw new LostException(cycle.getOwner().getName() + " loses!");
		}
	}
	
	protected void addCycle(Cycle cycle, Position pos) {
		pos.setValue(cycle);
		moves.get(cycle.getOwner()).add(pos);
		cycle.setPosition(pos.getX(), pos.getY());
		
		// Update observers
		changed();
	}
	
	public int getNumPlayers() {
		return players.size();
	}
	
	public Player getCurrentPlayer() {
		return(players.get(currentPlayer));
	}
	
	public void nextPlayer() {
		if (currentPlayer < players.size() - 1) {
			currentPlayer++;
		} else {
			currentPlayer = 0;
		}
	}
	
	public Cycle getCycle(int playerIndex) {
		return cycles.get(playerIndex);
	}
	
	public Cycle getCycle(Player player) {
		return playerCycles.get(player);
	}
	
	public List<Cycle> getCycles() {
		return Collections.unmodifiableList(cycles);
	}
	
	public List<Player> getPlayers() {
		return Collections.unmodifiableList(players);
	}
	
	public List<Position> getMoves(Cycle cycle) {
		return getMoves(cycle.getOwner());
	}
	
	public List<Position> getMoves(Player player) {
		return Collections.unmodifiableList(moves.get(player));
	}
	
	public List<Position> getNonLosingMoves(Cycle c) {
		List<Position> nonLosingMoves = new ArrayList<Position>();
		
		for (Position move : getValidMoves(c)) {
			if (move.getValue() == null) {
				nonLosingMoves.add(move);
			}
		}
		
		return nonLosingMoves;
	}
	
	/**
	 * Exact layout of the map and starting positions of players is left to
	 * subclasses to define. */
	public abstract void init() throws UnsupportedNumberOfPlayersException;
	
	/**
	 * Different map layouts can define new ways of moving.
	 * 
	 * @return List of valid moves.
	 */
	public abstract List<Position> getValidMoves(Cycle cycle);
}
